Skip to content

2.1. First Agent

What are you inspecting first?

You are inspecting and extending the smallest useful slice of the completed Ops Copilot reference: typed settings, an explicit model selector, a clear instruction, read-only incident tools, and a runnable ADK root agent. Later pages explain each capability; this page shows how the pieces compose before the capstone asks you to replace the fictional domain.

How is configuration parsed?

pydantic-settings validates the provider boundary once. The required path defaults to the OpenAI-compatible local endpoint exposed by Ollama:

model_provider: ModelProvider = ModelProvider.OPENAI_COMPATIBLE
model: str = Field(default="qwen3:4b", min_length=1)

# ``openai-compatible`` describes the ADK client contract, not the
# deployment topology. Point this URL directly at Ollama for the account-free
# first run, or at agentgateway when the governed data plane is introduced.
openai_base_url: str | None = Field(
    default="http://127.0.0.1:11434/v1",
    validation_alias=AliasChoices("OPENAI_BASE_URL"),
)
openai_api_key: SecretStr | None = Field(
    default=SecretStr("local-ollama"),
    validation_alias=AliasChoices("OPENAI_API_KEY"),
)

# Optional Gemini paths: an AI Studio API key, or Enterprise/Vertex via ADC
# with an explicit project and location.
google_api_key: SecretStr | None = Field(
    default=None,
    validation_alias=AliasChoices("GOOGLE_API_KEY"),
)
google_genai_use_enterprise: bool = Field(
    default=False,
    validation_alias=AliasChoices("GOOGLE_GENAI_USE_ENTERPRISE"),
)
google_cloud_project: str | None = Field(
    default=None,
    validation_alias=AliasChoices("GOOGLE_CLOUD_PROJECT"),
)
google_cloud_location: str | None = Field(
    default=None,
    validation_alias=AliasChoices("GOOGLE_CLOUD_LOCATION"),
)

The exact excerpt is build-checked against config.py. The provider is explicit: openai-compatible uses the local endpoint by default, while gemini is an optional provider selected deliberately.

How is the model selected?

The application has two explicit branches:

def build_model() -> str | BaseLlm:
    """Return the configured ADK model implementation.

    Gemini mode uses ADK's native integration. The default account-free mode
    uses ADK's OSS OpenAI-compatible client; ``OPENAI_BASE_URL`` chooses direct
    Ollama or an agentgateway route without changing application code.
    """
    if settings.model_provider is ModelProvider.GEMINI:
        retry_options = _retry_options()
        return Gemini(
            model=settings.model,
            retry_options=retry_options,
            client_kwargs=_gemini_client_kwargs(retry_options),
        )
    if not settings.openai_base_url or not settings.openai_api_key:
        raise ValueError(
            "AGENT_MODEL_PROVIDER=openai-compatible requires OPENAI_BASE_URL and OPENAI_API_KEY; "
            "run `mise run config:check` for the resolved configuration."
        )
    return ResilientOpenAILlm(
        model=settings.model,
        openai_base_url=settings.openai_base_url,
        openai_api_key=settings.openai_api_key,
        timeout_s=settings.model_timeout_s,
        retries=settings.max_retries,
    )

The required branch uses ADK's OpenAI-compatible adapter with qwen3:4b at http://127.0.0.1:11434/v1. Chapter 5 changes that endpoint to agentgateway without changing the application/model contract. The optional branch creates ADK's native Gemini client and applies its retry policy.

How is the root agent defined?

root_agent = Agent(
    model=build_model(),
    name="agentops_agent",
    description="An on-call Ops Copilot that triages and resolves incidents from a local dataset.",
    instruction=_instruction(),
    tools=[*_read_tools(), *ACTION_TOOLS, *MEMORY_TOOLS, skill_toolset()],
    # Callback lists chain with first-non-None-wins: the budget check runs before
    # redaction (a refused call needs no redaction), and usage recording returns
    # None so the PII pass still sees every response.
    before_model_callback=[enforce_token_budget, redact_request_pii],
    after_model_callback=[record_token_usage, redact_response_pii],
    before_tool_callback=validate_actions,
    after_tool_callback=secure_tool_output,
    on_model_error_callback=handle_model_error,
    on_tool_error_callback=handle_tool_error,
)

The composition is intentionally visible and build-checked against agent.py. _read_tools() returns direct read/knowledge functions locally or one governed MCP toolset when AGENT_MCP_URL is configured. Writes, long-term memory, and skill loading keep separate owners; policy is attached at the runtime boundary rather than hidden inside the prompt.

How does the ADK CLI discover it?

agents/python/src/agent/__init__.py imports root_agent. The adk CLI receives the package path and discovers that conventional name:

from .agent import root_agent

__all__ = ["root_agent"]

Do not put network calls or destructive setup in package import code. Discovery, tests, and tooling all import this module.

How do you run it with local Qwen3?

Install Ollama, pull the Apache-2.0 open-weight model, validate it from the repository root, then run the agent:

ollama pull qwen3:4b
mise run doctor:model
cd agents/python
mise run run
# or: mise run web

The default settings use AGENT_MODEL_PROVIDER=openai-compatible, AGENT_MODEL=qwen3:4b, OPENAI_BASE_URL=http://127.0.0.1:11434/v1, and a non-secret local marker. Start with List the open incidents. The expected behavior is a list_incidents tool call followed by a grounded summary. Wording can differ; invented incidents or skipped tools are failures.

The optional Gemini path sets AGENT_MODEL_PROVIDER=gemini, an explicit Gemini model, and either GOOGLE_API_KEY or Application Default Credentials. It is not required to complete this page.

Can you verify the agent without a model?

Yes. Construction, configuration errors, tools, callbacks, MCP/A2A application wiring, and state are covered offline:

mise run test

This is the default checkpoint. Interactive output is useful exploration, not proof that all boundaries work.

What is the page checkpoint?

Run the tests, then inspect root_agent.tools in a Python shell or test. Confirm that read, knowledge, action, memory, and skill toolsets are registered, that the default OpenAI-compatible path resolves to local Ollama, and that invalid provider combinations fail with an actionable message. Continue when both provider branches are intentional rather than environment-dependent surprises.